#----------------------------------
# VisualK-OS                       
#    Module: Screen Manager [v1.0] 
#    Author: Kevin Slaughter       
#----------------------------------

"""
[Revision History]
    v3.0: Implemented modular screen classes... ScreenManager class size
          was getting ridiculous.
    v2.0: Standardization, more complete debug mode implementation, misc. optimizations.
    v1.0: Initial implementation.
"""



### Lib/Header import
from VKCC_Constants import *
import globals, curses

### Screen object classes
from _screenMain           import cScreenMain
from _screenCfgMain        import cScreenCfgMain
from _screenCfgFBA         import cScreenCfgFBA
from _screenCfgSS          import cScreenCfgSS
from _screenCfgThor        import cScreenCfgThor
from _screenDisplayMapping import cScreenDisplayMapping
from _screenClientScan     import cScreenClientScan
from _screenManualFBA      import cScreenManualFBA


### Obj: Screen Manager
class cScreenManager(object):
    ### Screen objects
    scrMain           = cScreenMain()               # Main menu
    scrCfgMain        = cScreenCfgMain()            # Main Cfg menu
    scrCfgFBA         = cScreenCfgFBA()             # Cfg Menu: FBA
    scrCfgSS          = cScreenCfgSS()              # Cfg Menu: SS
    scrCfgThor        = cScreenCfgThor()            # Cfg Menu: Thor    
    scrDisplayMapping = cScreenDisplayMapping()     # Display GamePad Mapping
    scrClientScan     = cScreenClientScan()         # Slave/Device Client Scan
    scrManualFBA      = cScreenManualFBA()          # Manual FBA control menu


    ### Vars
    curScreen   = SCREEN_NULL	# Default/Invalid state
    redraw      = False		# Redraw is triggered as needed
    notifyTimer = 0		# Forces redraw after 500ms of data underneath


    ### changeScreen(): Simplification macro
    def changeScreen(self, newScreen):
        if (globals.DEBUGMODE):
            self.curScreen = SCREEN_DEBUG   # Override. This never changes in debug mode.
        else:
            self.curScreen = newScreen      # Assign new screen and redraw
            self.paintScreen()
        return


    ### drawTable(): Unified function for drawing tables in the curses UI
    def drawTable(self, sTable, iX, iY):
        if (globals.DEBUGMODE): return  # No curses UI in debug mode

        # Flood Bank Array color selection table
        if(sTable == "FBA"):
            self.menuPrep(5, "Flood Bank Array (FBA) Configuration")
            self.printText(2,4,  "Bank 0 ", T_HEADER)
            self.printText(2,5,  "   Lamp #0:    ", T_HEADER)
            self.printText(2,6,  "   Lamp #1:    ", T_HEADER)
            self.printText(2,7,  "   Lamp #2:    ", T_HEADER)
            self.printText(2,8,  "   Lamp #3:    ", T_HEADER)
            self.printText(2,10, "Bank 1 ", T_HEADER)
            self.printText(2,11, "   Lamp #0:    ", T_HEADER)
            self.printText(2,12, "   Lamp #1:    ", T_HEADER)
            self.printText(2,13, "   Lamp #2:    ", T_HEADER)
            self.printText(2,14, "   Lamp #3:    ", T_HEADER)
            return

        # Thor trigger threshold adjustment table
        elif(sTable == "THOR"):
            t=0; iOX=iX; iOY=iY
            for t in range(0,8):
                # Plot
                iX = iOX if (t%2==0) else (iOX+16)	# 2 columns
                iY = (iOY+(t/2))

                # Draw framework/column headers
                self.printText(2,4, "    [A]  [RT]       [A]  [RT]", T_HEADER); # Column headers
                self.printText(2,3, "* Drum Trigger Thresholds *")
                self.printText(iOX,11, "  Color  Selection      ", T_HEADER)
		
                # Label
                self.printText(iX,iY, "D"+str(t)); self.printText((iX+2),iY, "     ",T_ERROR)
                self.printText(iX,(iY+7), "D"+str(t)); self.printText((iX+2),(iY+7), "     ",T_ERROR)		
        return


    ### fillRect(x1,y1,x2,y2): Fill the given rectangle with solid color from the current theme.
    def fillRect(self,x1,y1,x2,y2):
        # Sanity checks
        if (x1<0): x1 = 0
        if (x2<0): x2 = 0
        if (y1<0): y1 = 0
        if (y2<0): y2 = 0
        if (x1>globals.MAX_X): x1 = globals.MAX_X
        if (x2>globals.MAX_X): x2 = globals.MAX_X
        if (y1>globals.MAX_Y): y1 = globals.MAX_Y
        if (y2>globals.MAX_Y): y2 = globals.MAX_Y

        # Fill region
        for y in range(y1,y2):
            for x in range(x1,x2):
                try: self.screen.addch(y,x,' ',curses.color_pair(1))
                except: return	# Skip over errors
        return


    ### fillScreen(): Fill screen with solid blocks of color from the current theme.
    def fillScreen(self):
        self.fillRect(0,0,globals.MAX_X,globals.MAX_Y)
        return


    ### getScreenBounds(): Lazy macro; Gets the current terminal dimensions in the globals object.
    def getScreenBounds(self):
	if (globals.DEBUGMODE):    # No screen to measure in debug mode
	    globals.MAX_Y = 0
	    globals.MAX_X = 0
	    return

        MAXYX = self.screen.getmaxyx()
        globals.MAX_Y = MAXYX[0]    # Max Y
        globals.MAX_X = MAXYX[1]    # Max X
        return


    ### keyHandler(): Process keystrokes based on
    def keyHandler(self):
        if (globals.DEBUGMODE): return  # No key handling required in debug mode

        # Get and parse key data
        c = self.screen.getch();	    # Get keypress, if any
        if (c == curses.ERR): return	# Exit if error

        ### Dispatch accordingly...
        if (self.curScreen == SCREEN_MAIN):		self.scrMain.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_CFG_MAIN):	self.scrCfgMain.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_CFG_FBA):	self.scrCfgFBA.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_CFG_SS):	        self.scrCfgSS.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_CFG_THOR):	self.scrCfgThor.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_MAPPING):	self.scrDisplayMapping.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_SCAN):		self.scrClientScan.handleKey(c, globals.SM)
        elif (self.curScreen == SCREEN_MANUAL_FBA):	self.scrManualFBA.handleKey(c, globals.SM)
        return


    ### menuPrep(): Lazy macro to draw the menu template
    def menuPrep(self, scrObj):
        if (globals.DEBUGMODE): return  # No curses UI in debug mode

        # Clear/Setup
        self.redraw = True
        self.setColorTheme(scrObj.color)  # Assert color theme
        self.screen.clear()               # Clear buffer
        self.fillScreen();                # Blank
        self.screen.border()	          # Draw simple frame border

        # Caption/Title & separator bar
        self.printText(1,1, "["+scrObj.title+"]", T_HEADER)
        self.printText(1,2, str('-'*(len(scrObj.title)+2)), T_HEADER)

        # Exit option, if applicable
        if (scrObj.hasExit): self.printText(1,(globals.MAX_Y-2), "0.) Exit", globals.T_HEADER)
        return

    
    
    ### paintScreen(): Display the current screen.
    def paintScreen(self):
        ### Draw screen contents
        if (self.curScreen == SCREEN_DEBUG): return  # No curses UI in debug mode

        # Screen-handling switch-point
        elif (self.curScreen == SCREEN_MAIN):		self.scrMain.paint(self)
        elif (self.curScreen == SCREEN_CFG_MAIN):	self.scrCfgMain.paint(self)
        elif (self.curScreen == SCREEN_CFG_FBA):	self.scrCfgFBA.paint(self)
        elif (self.curScreen == SCREEN_CFG_SS):         self.scrCfgSS.paint(self)
        elif (self.curScreen == SCREEN_CFG_THOR):	self.scrCfgThor.paint(self)        
        elif (self.curScreen == SCREEN_MAPPING):	self.scrDisplayMapping.paint(self)
        elif (self.curScreen == SCREEN_SCAN):		self.scrClientScan.paint(self)        
        elif (self.curScreen == SCREEN_MANUAL_FBA):	self.scrManualFBA.paint(self)


    ### printNA(): Standarization/lazy macro
    def printNA(self, x, y):
	globals.SM.printText(x,y,"[n/a]", T_ERROR)
	return


    ### printNotImplemented(): Standardization/lazy macro.
    def printNotImplemented(self, x, y):
	globals.SM.printText(x,y,"[NOT IMPLEMENTED]", T_ERROR)
	return


    ### printText(): Simpler means of printing to the screen than direct curses calls.
    def printText(self, iX, iY, sTxt="", iType=T_NORMAL, cF=""):
        if (globals.DEBUGMODE): return  # No curses UI in debug mode

        # Setup curses command
        iFlag = curses.A_BOLD	# All text is bold by default, for readability on a 3.5" TFT LCD.
        if ("s" in cF): iFlag = (iFlag | curses.A_STANDOUT)     # Bolder
        if ("u" in cF): iFlag = (iFlag | curses.A_UNDERLINE)    # Underline
        if ("f" in cF): iFlag = (iFlag | curses.A_BLINK)        # Blinking
        if ("l" in cF): iFlag = curses.A_NORMAL                 # Normal text                

        # Send to screen
        self.screen.addstr(iY,iX,sTxt,curses.color_pair(iType)|iFlag)
        return


    ### restoreConsole(): Ends curses session and restores terminal.
    def restoreConsole(self):
        # Debug mode does not require a console reset
        if not globals.DEBUGMODE:
            curses.nocbreak(); curses.echo()    # Setup console as needed
            self.screen.keypad(False); curses.endwin()
        return


    ### setupCurses(): Prepares terminal for curses-based UI.
    def setupCurses(self):
        if (globals.DEBUGMODE): return    # No curses UI in debug mode

        # Setup curses UI
        self.screen = curses.initscr()
        curses.curs_set(0); curses.start_color()
        curses.noecho(); curses.cbreak(); self.screen.keypad(True);
        self.screen.clear(); self.screen.refresh(); self.screen.nodelay(True)

        # Get screen dimensions
        MAXYX = self.screen.getmaxyx()
        globals.MAX_X = MAXYX[1]; globals.MAX_Y = MAXYX[0]

        # Init custom colors. These sit outside the index of the color themes,
        # so no matter what we set it to, these are available.
        curses.init_color(CB_BLUE+1,  0,0,1000);	    curses.init_pair(CB_BLUE,    CB_BLUE, CB_BLUE)
        curses.init_color(CB_CYAN,    0,1000,1000);         curses.init_pair(CB_CYAN,    CB_CYAN,CB_CYAN)
        curses.init_color(CB_GREEN,   0,1000,0); 	    curses.init_pair(CB_GREEN,   CB_GREEN,CB_GREEN)
        curses.init_color(CB_MAGENTA, 1000,0,500); 	    curses.init_pair(CB_MAGENTA, CB_MAGENTA,CB_MAGENTA)
        curses.init_color(CB_ORANGE,  1000,500,0); 	    curses.init_pair(CB_ORANGE,  CB_ORANGE,CB_ORANGE)
        curses.init_color(CB_PURPLE,  800,0,1000); 	    curses.init_pair(CB_PURPLE,  CB_PURPLE,CB_PURPLE)
        curses.init_color(CB_RED,     1000,0,0); 	    curses.init_pair(CB_RED,     CB_RED,CB_RED)
        curses.init_color(CB_WHITE,   1000,1000,1000);      curses.init_pair(CB_WHITE,   CB_WHITE,CB_WHITE)
        curses.init_color(CB_YELLOW,  1000,1000,0);         curses.init_pair(CB_YELLOW,  CB_YELLOW,CB_YELLOW)
        curses.init_color(curses.COLOR_YELLOW, 800,900,0)   # The default yellow is an ugly brownish tone.
	
	# Indicate that a curses session has been created
	globals.CURSES_UI = True
        return


    ### setColorTheme(): Set the active color theme.
    def setColorTheme(self, iTheme):
        if (globals.DEBUGMODE): return    # No curses UI in debug mode

        # Assign theme-grouped colors
        if(iTheme == 0):	# Black theme
            curses.init_color(0, 0,0,0)
            curses.init_pair(1, curses.COLOR_BLACK,curses.COLOR_BLACK)	# Solid block
            curses.init_pair(2, curses.COLOR_WHITE,curses.COLOR_BLACK)	# Normal text
            curses.init_pair(3, curses.COLOR_BLACK,curses.COLOR_WHITE)	# Header text
            curses.init_pair(4, curses.COLOR_BLACK,curses.COLOR_YELLOW)	# Highlighted text
            curses.init_pair(5, curses.COLOR_WHITE,curses.COLOR_BLUE)		# Error text
        if(iTheme == 1):	# Blue theme
            curses.init_pair(1, curses.COLOR_BLUE,curses.COLOR_BLUE)
            curses.init_pair(2, curses.COLOR_CYAN,curses.COLOR_BLUE)
            curses.init_pair(3, curses.COLOR_CYAN,curses.COLOR_BLACK)
            curses.init_pair(4, curses.COLOR_YELLOW,curses.COLOR_BLACK)
            curses.init_pair(5, curses.COLOR_RED,curses.COLOR_BLUE)
        elif(iTheme == 2):	# Green theme
            curses.init_pair(1, curses.COLOR_GREEN,curses.COLOR_GREEN)
            curses.init_pair(2, curses.COLOR_WHITE,curses.COLOR_GREEN)
            curses.init_pair(3, curses.COLOR_GREEN,curses.COLOR_BLACK)
            curses.init_pair(4, curses.COLOR_YELLOW,curses.COLOR_BLACK)
            curses.init_pair(5, curses.COLOR_RED,curses.COLOR_BLACK)
        elif(iTheme == 3):	# Yellow theme
            curses.init_pair(1, curses.COLOR_YELLOW,curses.COLOR_YELLOW)
            curses.init_pair(2, curses.COLOR_BLACK,curses.COLOR_YELLOW)
            curses.init_pair(3, curses.COLOR_YELLOW,curses.COLOR_BLACK)
            curses.init_pair(4, curses.COLOR_WHITE,curses.COLOR_YELLOW)
            curses.init_pair(5, curses.COLOR_BLACK,curses.COLOR_MAGENTA)
        elif(iTheme == 4):	# Purple theme
            curses.init_pair(1, CB_PURPLE,CB_PURPLE)
            curses.init_pair(2, curses.COLOR_WHITE,CB_PURPLE)
            curses.init_pair(3, curses.COLOR_MAGENTA,curses.COLOR_BLACK)
            curses.init_pair(4, curses.COLOR_WHITE,curses.COLOR_MAGENTA)
            curses.init_pair(5, curses.COLOR_RED,CB_PURPLE)
        elif(iTheme == 5):  # Orange theme
            curses.init_pair(1, CB_ORANGE,CB_ORANGE)
            curses.init_pair(2, curses.COLOR_BLACK,CB_ORANGE)
            curses.init_pair(3, CB_ORANGE,curses.COLOR_BLACK)
            curses.init_pair(4, CB_RED,curses.COLOR_WHITE)
            curses.init_pair(5, CB_RED,curses.COLOR_BLACK)

        # Store
        globals.curTheme = iTheme
        return

    
    ### showNotification(sMsg): Display a popup message
    def showNotification(self, sMsg):
        # Display and exit for debug mode
        if (globals.DEBUGMODE):
            print("[NOTIFICATION] "+sMsg)
	    return
        
        ### Draw on screen
        # Truncate string, if needed
        if (len(sMsg)>(globals.MAX_X-3)): sMsg = sMsg[(globals.MAX_X-3):]
	
        # Display info box at bottom of screen
	iMsgLen = len(sMsg)
	globals.SM.printText(0,(globals.MAX_Y-4),"[Notification]",T_HIGHLIGHT)
        globals.SM.printText(0,(globals.MAX_Y-3),(" /"+("-"*(globals.MAX_X-5))+"\\ "),T_HIGHLIGHT)
        globals.SM.printText(0,(globals.MAX_Y-2),("| "+sMsg),T_HIGHLIGHT)
	globals.SM.printText((iMsgLen+1),(globals.MAX_Y-2),(" "*(globals.MAX_X-iMsgLen-3)+"|"),T_HIGHLIGHT)
        globals.SM.printText(0,(globals.MAX_Y-1),(" \\"+("-"*(globals.MAX_X-5))+"/ "),T_HIGHLIGHT)
	
	# Last bits
        self.notifyTimer = 2000	    # Start timer (2sec)
	self.screen.refresh()	    # Update display
        return
    
    
    ### update(): Redraw screen, if needed.
    def update(self):
        # No redraws needed in debug mode
        if (globals.DEBUGMODE): self.redraw = False; return

        # Notify timer... Clears pop-up notificatons
        if (self.notifyTimer>0):
            self.notifyTimer -= 1	# Step

            # Check and redraw, if timer has elapsed.
            if (self.notifyTimer <= 0):
                self.redraw = True	# Trigger redraw to erase notification

        # A full redraw is usually only needed during complete screen changes.
        if (self.redraw):
            self.paintScreen()      # Re-generate screen data. Always sets redraw to True, so...
            self.redraw = False     # ^^^ Since we're already handling a redraw... It's not needed.
	    
            # Display slave emulation status in the upper-right corner.
            if (globals.emulateSlaves):
                globals.SM.printText((globals.MAX_X-20),0,"[EMULATING DEVICES]")

        # Update the display, in case any element data has changed.
        self.screen.refresh()
        return
